from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd
# --- Data loading and cleaning ---
df = pd.read_csv(
"https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-22/Marvel-Movies.csv"
)
percent_cols = [
'% budget recovered', 'critics % score', 'audience % score',
'audience vs critics % deviance', '% gross from opening weekend',
'% budget opening weekend'
]
for col in percent_cols:
df[col] = (
df[col]
.astype(str)
.str.replace('%', '', regex=False)
.str.replace(',', '.', regex=False)
)
df[col] = pd.to_numeric(df[col], errors='coerce')
money_cols = [
'worldwide gross', 'budget', 'domestic gross ($m)',
'international gross ($m)', 'opening weekend ($m)', 'second weekend ($m)'
]
for col in money_cols:
df[col] = (
df[col]
.astype(str)
.str.replace('$', '', regex=False)
.str.replace('m', '', regex=False)
.str.replace(',', '', regex=False)
)
df[col] = pd.to_numeric(df[col], errors='coerce')
df['1st vs 2nd weekend drop off'] = (
df['1st vs 2nd weekend drop off']
.astype(str)
.str.replace('%', '', regex=False)
.str.replace(',', '.', regex=False)
)
df['1st vs 2nd weekend drop off'] = pd.to_numeric(
df['1st vs 2nd weekend drop off'], errors='coerce'
)
df['year'] = pd.to_numeric(df['year'], errors='coerce')
# --- Color palette: yellow and red ---
yellow = "#ffd600"
red = "#d90429"
# --- Dropdown options for category and film ---
def get_category_options():
return [{"label": "All", "value": "All"}] + [
{"label": cat, "value": cat}
for cat in sorted(df["category"].dropna().unique())
]
def get_film_options(category):
if category == "All":
films = df["film"].unique()
else:
films = df[df["category"] == category]["film"].unique()
return [{"label": "All", "value": "All"}] + [
{"label": f, "value": f} for f in sorted(films)
]
# --- KPI style: gray background ---
kpi_style = {
"borderRadius": "18px",
"padding": "24px 18px",
"color": "white",
"fontWeight": "bold",
"boxShadow": "0 4px 24px 0 rgba(0,0,0,0.4)",
"marginBottom": "18px",
"textAlign": "center",
"fontSize": "1.2rem",
"background": "#232323"
}
# --- Card style for chart containers ---
card_style = {
"backgroundColor": "#181818",
"borderRadius": "18px",
"boxShadow": "0 4px 24px 0 rgba(0,0,0,0.4)",
"padding": "24px",
"marginBottom": "32px"
}
external_stylesheets = [
dbc.themes.DARKLY,
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
]
app = Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = dbc.Container([
# Google Fonts for Bangers
html.Link(
rel="stylesheet",
href="https://fonts.googleapis.com/css2?family=Bangers&display=swap"
),
html.H1([
html.I(className="fa-solid fa-film", style={"marginRight": "12px"}),
'Marvel Movies: Key Insights'
],
className="my-4 text-center",
style={
"color": yellow,
"fontWeight": "bold",
"fontFamily": "'Bangers', 'Arial Black', cursive, sans-serif",
"fontSize": "3.2rem",
"letterSpacing": "0.08em",
"textShadow": "2px 2px 8px #d90429, 0 0 2px #000",
"marginBottom": "0.5em"
}
),
# --- KPIs with gray background ---
dbc.Row([
dbc.Col(html.Div([
html.Div("💰 Total Budget", style={"fontSize": "1.5rem", "opacity": 0.8}),
html.Div(id="kpi-budget", style={"fontSize": "2rem"})
], style=kpi_style), md=3),
dbc.Col(html.Div([
html.Div("🌏Total Worldwide Gross", style={"fontSize": "1.5rem", "opacity": 0.8}),
html.Div(id="kpi-gross", style={"fontSize": "2rem"})
], style=kpi_style), md=3),
dbc.Col(html.Div([
html.Div("⭐ Avg. Critics Score", style={"fontSize": "1.5rem", "opacity": 0.8}),
html.Div(id="kpi-critics", style={"fontSize": "2rem"})
], style=kpi_style), md=3),
dbc.Col(html.Div([
html.Div("👨👩👧👧Avg. Audience Score", style={"fontSize": "1.5rem", "opacity": 0.8}),
html.Div(id="kpi-audience", style={"fontSize": "2rem"})
], style=kpi_style), md=3),
], className="mb-4"),
# --- Dropdowns below KPIs ---
dbc.Row([
dbc.Col([
html.Label("Category", style={"color": "white", "fontWeight": "bold", "marginBottom": "6px"}),
dbc.Select(
id="category-filter",
options=get_category_options(),
value="All",
style={
"backgroundColor": "#232323",
"color": "white",
"fontWeight": "bold",
"marginBottom": "12px",
"border": "2px solid #888",
"height": "32px",
"fontSize": "0.9rem",
"padding": "2px 8px",
"width": "100%"
}
)
], md=3),
dbc.Col([
html.Label("Film", style={"color": "white", "fontWeight": "bold", "marginBottom": "6px"}),
dbc.Select(
id="film-filter",
options=get_film_options("All"),
value="All",
style={
"backgroundColor": "#232323",
"color": "white",
"fontWeight": "bold",
"marginBottom": "12px",
"border": "2px solid #888",
"height": "32px",
"fontSize": "0.9rem",
"padding": "2px 8px",
"width": "100%"
}
)
], md=3),
], className="mb-4 justify-content-center"),
# --- Charts ---
dbc.Row([
dbc.Col([
html.Div([
html.P("Production budget of the selected Marvel movies.", style={"fontWeight": "bold", "color": yellow, "marginBottom": "12px"}),
dcc.Graph(id="fig1", config={"displayModeBar": False})
], style=card_style)
], md=6),
dbc.Col([
html.Div([
html.P(id="pie-desc", style={"fontWeight": "bold", "color": yellow, "marginBottom": "12px"}),
dcc.Graph(id="fig2", config={"displayModeBar": False})
], style=card_style)
], md=6),
], className="gy-4"),
dbc.Row([
dbc.Col([
html.Div([
html.P("Relationship between critics and audience scores, with bubble size representing worldwide gross.", style={"fontWeight": "bold", "color": yellow, "marginBottom": "12px"}),
dcc.Graph(id="fig3", config={"displayModeBar": False})
], style=card_style)
], md=6),
dbc.Col([
html.Div([
html.P("Trends in budget and worldwide gross over the years for the selected Marvel movies.", style={"fontWeight": "bold", "color": yellow, "marginBottom": "12px"}),
dcc.Graph(id="fig4", config={"displayModeBar": False})
], style=card_style)
], md=6),
], className="gy-4"),
], fluid=True, style={
"backgroundColor": "#111111",
"padding": "48px" # even padding on all sides
})
@app.callback(
Output("film-filter", "options"),
Output("film-filter", "value"),
Input("category-filter", "value"),
Input("film-filter", "value"),
)
def update_film_dropdown(category, current_film):
options = get_film_options(category)
values = [opt["value"] for opt in options]
value = current_film if current_film in values else "All"
return options, value
@app.callback(
[
Output("kpi-budget", "children"),
Output("kpi-gross", "children"),
Output("kpi-critics", "children"),
Output("kpi-audience", "children"),
Output("fig1", "figure"),
Output("pie-desc", "children"),
Output("fig2", "figure"),
Output("fig3", "figure"),
Output("fig4", "figure"),
],
[Input("category-filter", "value"), Input("film-filter", "value")]
)
def update_dashboard(selected_category, selected_film):
if selected_category == "All" and selected_film == "All":
dff = df
pie_title = "Domestic vs. International Gross (All Movies)"
pie_desc = "Domestic vs. international gross share for the selected Marvel movies."
elif selected_category != "All" and selected_film == "All":
dff = df[df["category"] == selected_category]
pie_title = f"Domestic vs. International Gross ({selected_category})"
pie_desc = f"Domestic vs. international gross share for category: {selected_category}."
elif selected_film != "All":
dff = df[df["film"] == selected_film]
pie_title = f"Domestic vs. International Gross ({selected_film})"
pie_desc = f"Domestic vs. international gross share for movie: {selected_film}."
else:
dff = df
pie_title = "Domestic vs. International Gross (All Movies)"
pie_desc = "Domestic vs. international gross share for Marvel movies."
kpi_budget = f"${dff['budget'].sum():,.0f}M"
kpi_gross = f"${dff['worldwide gross'].sum():,.0f}M"
kpi_critics = f"{dff['critics % score'].mean():.1f}%"
kpi_audience = f"{dff['audience % score'].mean():.1f}%"
fig1 = px.bar(
dff.sort_values('budget', ascending=False),
y='film',
x='budget',
color_discrete_sequence=[yellow],
orientation='h',
title='',
labels={'budget': 'Budget (million USD)', 'film': 'Movie'}
)
fig1.update_yaxes(autorange="reversed")
fig1.update_layout(
plot_bgcolor='#232323',
paper_bgcolor='#232323',
font_color='white',
title_font_color=yellow,
legend_font_color='white',
xaxis=dict(color='white', gridcolor='#444'),
yaxis=dict(color='white', gridcolor='#444'),
margin=dict(l=80, r=30, t=60, b=40)
)
total_domestic = dff['domestic gross ($m)'].sum()
total_international = dff['international gross ($m)'].sum()
pie_df = pd.DataFrame({
'Type': ['Domestic', 'International'],
'Gross': [total_domestic, total_international]
})
fig2 = px.pie(
pie_df,
names='Type',
values='Gross',
color='Type',
color_discrete_map={'Domestic': yellow, 'International': red},
title='',
hole=0.7
)
fig2.update_traces(textinfo='percent+label')
fig2.update_layout(
plot_bgcolor='#262626',
paper_bgcolor='#262626',
font_color='white',
title_font_color=yellow,
legend_font_color='white',
margin=dict(l=40, r=40, t=60, b=40),
showlegend=False
)
fig3 = px.scatter(
dff,
x='critics % score',
y='audience % score',
color='category',
size='worldwide gross',
title='',
labels={
'critics % score': 'Critics Score (%)',
'audience % score': 'Audience Score (%)'
},
hover_name='film'
)
fig3.update_traces(textposition='top center', textfont=dict(color='white', size=12))
fig3.update_layout(
plot_bgcolor='#292929',
paper_bgcolor='#292929',
font_color='white',
title_font_color=yellow,
legend_font_color='white',
xaxis=dict(color='white', gridcolor='#444'),
yaxis=dict(color='white', gridcolor='#444'),
margin=dict(l=60, r=30, t=60, b=40),
)
dff_sorted = dff.sort_values('year')
fig4 = px.line(
dff_sorted,
x='year',
y=['budget', 'worldwide gross'],
markers=True,
line_shape="linear",
title='',
labels={'value': 'Amount (million USD)', 'year': 'Year', 'variable': 'Metric'},
color_discrete_sequence=[yellow, red]
)
fig4.update_layout(
plot_bgcolor='#2c2c2c',
paper_bgcolor='#2c2c2c',
font_color='white',
title_font_color=yellow,
legend_font_color='white',
xaxis=dict(color='white', gridcolor='#444'),
yaxis=dict(color='white', gridcolor='#444'),
margin=dict(l=60, r=30, t=60, b=40)
)
return kpi_budget, kpi_gross, kpi_critics, kpi_audience, fig1, pie_desc, fig2, fig3, fig4
if __name__ == "__main__":
app.run(debug=False)